using System;
using System.Collections.Generic;
using System.Reflection;
using NFreeMarker.Core;
using NFreeMarker.Ext.Util;
using NFreeMarker.Log;
using NFreeMarker.Template;
using NFreeMarker.Template.Utility;

namespace NFreeMarker.Ext.Beans
{
    /**
     * A class that will wrap an arbitrary object into {@link freemarker.template.TemplateHashModel}
     * interface allowing calls to arbitrary property getters and invocation of
     * accessible methods on the object from a template using the
     * <tt>object.foo</tt> to access properties and <tt>object.bar(arg1, arg2)</tt> to
     * invoke methods on it. You can also use the <tt>object.foo[index]</tt> syntax to
     * access indexed properties. It uses Beans {@link java.beans.Introspector}
     * to dynamically discover the properties and methods. 
     * @author Attila Szegedi
     * @version $Id: BeanModel.java,v 1.49.2.4 2006/11/12 10:20:37 szegedia Exp $
     */
    public class BeanModel : ITemplateHashModelEx, IAdapterTemplateModel, IWrapperTemplateModel
    {
        private static readonly Logger Logger = Logger.GetLogger("freemarker.beans");
        protected readonly object Object;
        protected readonly BeansWrapper Wrapper;
        
        // We use this to represent an unknown value as opposed to known value of null (JR)
        protected static readonly ITemplateModel Unknown = new SimpleScalar("UNKNOWN"); // NOTE: Was package
        
        //static final ModelFactory FACTORY =
        //    new ModelFactory()
        //    {
        //        public TemplateModel create(object object, ObjectWrapper wrapper)
        //        {
        //            return new BeanModel(object, (BeansWrapper)wrapper);
        //        }
        //    };

        // Cached template models that implement member properties and methods for this
        // instance. Keys are FeatureDescriptor instances (from classCache values),
        // values are either ReflectionMethodModels/ReflectionScalarModels
        private Dictionary<object, ITemplateModel> _memberMap;

        /**
         * Creates a new model that wraps the specified object. Note that there are
         * specialized subclasses of this class for wrapping arrays, collections,
         * enumeration, iterators, and maps. Note also that the superclass can be
         * used to wrap string objects if only scalar functionality is needed. You
         * can also choose to delegate the choice over which model class is used for
         * wrapping to {@link BeansWrapper#wrap(object)}.
         * @param object the object to wrap into a model.
         * @param wrapper the {@link BeansWrapper} associated with this model.
         * Every model has to have an associated {@link BeansWrapper} instance. The
         * model gains many attributes from its wrapper, including the caching 
         * behavior, method exposure level, method-over-item shadowing policy etc.
         */
        public BeanModel(object obj, BeansWrapper wrapper)
        {
            Object = obj;
            Wrapper = wrapper;
            if (obj == null) {
                return;
            }
            wrapper.IntrospectClass(obj.GetType());
        }

        /**
         * Uses Beans introspection to locate a property or method with name
         * matching the key name. If a method or property is found, it is wrapped
         * into {@link freemarker.template.TemplateMethodModelEx} (for a method or
         * indexed property), or evaluated on-the-fly and the return value wrapped
         * into appropriate model (for a simple property) Models for various
         * properties and methods are cached on a per-class basis, so the costly
         * introspection is performed only once per property or method of a class.
         * (Side-note: this also implies that any class whose method has been called
         * will be strongly referred to by the framework and will not become
         * unloadable until this class has been unloaded first. Normally this is not
         * an issue, but can be in a rare scenario where you create many classes on-
         * the-fly. Also, as the cache grows with new classes and methods introduced
         * to the framework, it may appear as if it were leaking memory. The
         * framework does, however detect class reloads (if you happen to be in an
         * environment that does this kind of things--servlet containers do it when
         * they reload a web application) and flushes the cache. If no method or
         * property matching the key is found, the framework will try to invoke
         * methods with signature
         * <tt>non-void-return-type get(java.lang.string)</tt>,
         * then <tt>non-void-return-type get(java.lang.object)</tt>, or 
         * alternatively (if the wrapped object is a resource bundle) 
         * <tt>object getObject(java.lang.string)</tt>.
         * @throws TemplateModelException if there was no property nor method nor
         * a generic <tt>get</tt> method to invoke.
         */
        public ITemplateModel Get(string key)
        {
            Type type = Object.GetType();
            Dictionary<object, object> classInfo = Wrapper.GetClassKeyMap(type);
            ITemplateModel retval = null;
            
            try
            {
                if (Wrapper.IsMethodsShadowItems())
                {
                    object fd = classInfo.GetValueOrDefault<object, object>(key);
                    if (fd != null)
                    {
                        retval = InvokeThroughDescriptor(fd, classInfo);
                    }
                    else
                    {
                        retval = InvokeGenericGet(classInfo, type, key);
                    }
                }
                else
                {
                    ITemplateModel model = InvokeGenericGet(classInfo, type, key);
                    ITemplateModel nullModel = Wrapper.Wrap(null);
                    if (model != nullModel && model != Unknown) 
                    {
                        return model;
                    }
                    object fd = classInfo.GetValueOrDefault<object, object>(key);
                    if (fd != null)
                    {
                        retval = InvokeThroughDescriptor(fd, classInfo);
                        if (retval == Unknown && model == nullModel)
                        {
                            // This is the (somewhat subtle) case where the generic get() returns null
                            // and we have no bean info, so we respect the fact that
                            // the generic get() returns null and return null. (JR)
                            retval = nullModel;
                        }
                    }
                }
                if (retval == Unknown)
                {
                    if (Wrapper.IsStrict())
                    {
                        throw new InvalidPropertyException("No such bean property: " + key);
                    }
                    if (Logger.IsDebugEnabled())
                    {
                        LogNoSuchKey(key, classInfo);
                    }
                    retval = Wrapper.Wrap(null);
                }
                return retval;
            }
            catch (TemplateModelException e)
            {
                throw;
            }
            catch (Exception e)
            {
                throw new TemplateModelException("get(" + key + ") failed on " +
                    "instance of " + Object.GetType().FullName, e);
            }
        }

        private void LogNoSuchKey(string key, Dictionary<object, object> keyMap)
        {
            Logger.Debug("Key '" + key + "' was not found on instance of " +
                Object.GetType().FullName + ". Introspection information for " +
                "the class is: " + keyMap);
        }
        
        ///**
        // * Whether the model has a plain get(string) or get(object) method
        // */
        
        //protected boolean hasPlainGetMethod() {
        //    return wrapper.getClassKeyMap(object.getClass()).get(BeansWrapper.GenericGetKey) != null;
        //}
        
        private ITemplateModel InvokeThroughDescriptor(object desc, Dictionary<object, object> classInfo)
        {
            // See if this particular instance has a cached implementation
            // for the requested feature descriptor
            ITemplateModel member;
            lock (this)
            {
                if (_memberMap != null)
                {
                    member = _memberMap.GetValueOrDefault<object, ITemplateModel>(desc);
                }
                else
                {
                    member = null;
                }
            }

            if(member != null)
                return member;

            ITemplateModel retval = Unknown;
            // TODO: if (desc is IndexedPropertyDescriptor)
            //{
            //    Method readMethod = 
            //        ((IndexedPropertyDescriptor)desc).getIndexedReadMethod(); 
            //    retval = member = 
            //        new SimpleMethodModel(object, readMethod, 
            //                BeansWrapper.getArgTypes(classInfo, readMethod), _wrapper);
            //}
            //else 
            if (desc is PropertyInfo)
            {
                var pd = (PropertyInfo) desc;
                retval = Wrapper.InvokeMethod(Object, pd.GetGetMethod(), null);
                // (member == null) condition remains, as we don't cache these
            }
            else if (desc is FieldInfo)
            {
                retval = Wrapper.Wrap(((FieldInfo) desc).GetValue(Object));
                // (member == null) condition remains, as we don't cache these
            }
            else if (desc is MethodInfo)
            {
                var method = (MethodInfo) desc;
                retval = member = new SimpleMethodModel(Object, method, 
                    BeansWrapper.GetArgTypes(classInfo, method), Wrapper);
            }
            else if (desc is MethodMap)
            {
                retval = member = 
                    new OverloadedMethodModel(Object, (MethodMap) desc);
            }
            
            // If new cacheable member was created, cache it
            if (member != null) 
            {
                lock (this)
                {
                    if (_memberMap == null)
                    {
                        _memberMap = new Dictionary<object, ITemplateModel>();
                    }
                    _memberMap.Add(desc, member);
                }
            }
            return retval;
        }

        protected ITemplateModel InvokeGenericGet(Dictionary<object, object> keyMap, Type type, string key)
        {
            var genericGet = (MethodInfo) keyMap.GetValueOrDefault<object, object>(BeansWrapper.GenericGetKey);
            if (genericGet == null)
            {
                return Unknown;
            }
            return Wrapper.InvokeMethod(Object, genericGet, new object[] { key });
        }

        protected ITemplateModel Wrap(object obj)
        {
            return Wrapper.GetOuterIdentity().Wrap(obj);
        }
        
        protected object Unwrap(ITemplateModel model)
        {
            return Wrapper.Unwrap(model);
        }

        /**
         * Tells whether the model is empty. It is empty if either the wrapped 
         * object is null, or it is a Boolean with false value.
         */
        public virtual bool IsEmpty()
        {
            if (Object is string)
            {
                return ((string) Object).Length == 0;
            }
            //if (object is Collection)
            //{
            //    return ((Collection) object).isEmpty();
            //}
            //if (object is Map)
            //{
            //    return ((Map) object).isEmpty();
            //}
            //return object == null || Boolean.FALSE.equals(object);
            throw new NotImplementedException();
        }
        
        public object GetAdaptedObject(Type type)
        {
            return Object;
        }

        public object GetWrappedObject()
        {
            return Object;
        }

        public virtual int Size()
        {
            return Wrapper.KeyCount(Object.GetType());
        }

        public ITemplateCollectionModel Keys()
        {
            return new CollectionAndSequence(new SimpleSequence(KeySet(), Wrapper));
        }

        public ITemplateCollectionModel Values()
        {
            var values = new List<ITemplateModel>(Size());
            ITemplateModelIterator it = Keys().Iterator();
            while (it.HasNext())
            {
                string key = ((ITemplateScalarModel) it.Next()).GetAsString();
                values.Add(Get(key));
            }
            return new CollectionAndSequence(new SimpleSequence(values, Wrapper));
        }
        
        //public string toString() {
        //    return object.toString();
        //}

        /**
         * Helper method to support TemplateHashModelEx. Returns the Set of
         * Strings which are available via the TemplateHashModel
         * interface. Subclasses that override <tt>invokeGenericGet</tt> to
         * provide additional hash keys should also override this method.
         */
        protected virtual HashSet<object> KeySet()
        {
            return Wrapper.KeySet(Object.GetType());
        }    
    }
}